package com.nulldev.util.uri.server.v2;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.URLConnection;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.regex.Pattern;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import com.nulldev.util.crypto.encryptAPI.aes.AESCipher;
import com.nulldev.util.crypto.encryptAPI.manage.EncryptAPI;
import com.nulldev.util.crypto.encryptAPI.rsa.RSA;
import com.nulldev.util.io.FileUtil;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.io.IOUtils.ChunkedEvent;
import com.nulldev.util.io.MimeType;
import com.nulldev.util.io.ZipUtils;
import com.nulldev.util.java.DataSet;
import com.nulldev.util.java.JVMUtil;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.math.MathUtil;
import com.nulldev.util.net.IPUtil;
import com.nulldev.util.other.ArrayUtils;
import com.nulldev.util.other.ByteUtils;
import com.nulldev.util.other.RandomUtils;
import com.nulldev.util.string.StringsUtil;
import com.nulldev.util.threading.legacy.ThreadExecutor;
import com.nulldev.util.threading.v3.RunnableWorker;
import com.nulldev.util.threading.v3.ThreadDispatcher;
import com.nulldev.util.uri.URIUtil2;
import com.nulldev.util.uri.server.HttpPage;
import com.nulldev.util.uri.server.RequestType;

public class HttpServer2 {
	
	public static enum SSLMode {
		LEGACY_OPTION,
		USE_NEAREST_PORT_POSITIVE,
		USE_NEAREST_PORT_NEGATIVE,
	}
	
	public static final float VERSION = 2.1f;
	
	private final static Logger log = LoggerFactory.getLogger(true, true);
	
	private int connectionsLimit = -1;
	
	private int connections;
	
	private final ThreadDispatcher td = new ThreadDispatcher(true);
	
	public static final long UNLIMITED_CONNECTIONS = -1;
	public static final String KEY_WEB_SERVER_NAME = "$HTTPServer2.WebServerName";
	
	private final HashMap<String, String> settings = new HashMap<String, String>();
	
	private String override = null;
	
	private ServerSocket generateSSLSocket() throws Exception {
		URIUtil2.initSSL();
		final SSLContext context = SSLContext.getInstance("TLSv1.2");
		final TrustManager[] trustAllCerts = new TrustManager[] {
				new X509TrustManager() {
					public void checkClientTrusted(final java.security.cert.X509Certificate[] certs, final String authType) {
					}

					public void checkServerTrusted(final java.security.cert.X509Certificate[] certs, final String authType) {
					}

					public java.security.cert.X509Certificate[] getAcceptedIssuers() {
						return null;
					}
				}
		};
		context.init(URIUtil2.getKeyManager(), trustAllCerts, new RandomUtils.nSecureRandom());
		final SSLServerSocketFactory ssl = (SSLServerSocketFactory) context.getServerSocketFactory();
		final SSLServerSocket socket = (SSLServerSocket) ssl.createServerSocket();
		socket.setEnabledCipherSuites(ssl.getSupportedCipherSuites());
		return socket;
	}
	
	private ServerSocket generateStandardSocket() throws Exception {
		final ServerSocket socket = new ServerSocket();
		return socket;
	}
	
	private final ServerSocket socket;
	private final SSLServerSocket httpsSocket;
	
	public HttpServer2(final int port, final boolean open) throws Exception {
		this(false, port, (open ? IPUtil.getLocalHostLANAddress() : IPUtil.getInternetLocalIP()),
				SSLMode.LEGACY_OPTION);
	}
	
	public HttpServer2(final boolean ssl, final int port, final boolean open) throws Exception {
		this(ssl, port, (open ? IPUtil.getLocalHostLANAddress() : IPUtil.getInternetLocalIP()),
				SSLMode.LEGACY_OPTION);
	}
	
	public HttpServer2(final boolean ssl, final int port, final boolean open, final SSLMode mode) throws Exception {
		this(ssl, port, (open ? IPUtil.getLocalHostLANAddress() : IPUtil.getInternetLocalIP()),
				mode);
	}
	
	public HttpServer2(final boolean ssl, final int port, final InetAddress addr,
			final SSLMode mode) throws Exception {
		this.addr = addr;
		this.port = port;
		this.settings.put("$HTTPServer2.WebServerName", "Default HTTPServer2");
		if (mode == SSLMode.LEGACY_OPTION) {
			this.socket = (ssl ? this.generateSSLSocket() : this.generateStandardSocket());
			this.httpsSocket = null;
		}else {
			this.socket = this.generateStandardSocket();
			this.httpsSocket = (SSLServerSocket) this.generateSSLSocket();
		}
		this.socket.bind(new InetSocketAddress(addr, port));
		int sslPORT = 0;
		if (this.httpsSocket != null) {
			if (mode.equals(SSLMode.USE_NEAREST_PORT_NEGATIVE)) {
				sslPORT = IPUtil.nextFreePortIterated(port - 99, port - 1);
			}else {
				sslPORT = IPUtil.nextFreePortIterated(port + 1, port + 99);
			}
			this.httpsSocket.bind(new InetSocketAddress(addr, sslPORT));
		}
		this.log.info("Server is now exposed. [ip=" + this.socket.getInetAddress().getHostAddress() + ", port=" + port + ",ssl=" + ssl + "]");
		if (this.httpsSocket != null) {
			this.log.info("SSL Server is now exposed. [ip=" 
					+ this.socket.getInetAddress().getHostAddress() + ", port=" + sslPORT + ",ssl=" + ssl + "]");
		}
		ThreadExecutor.execute(new Runnable() {
			
			@Override
			public void run() {
				while (true) {
					try {
						final Socket s = HttpServer2.this.socket.accept();
						HttpServer2.this.td.addTask(RunnableWorker.fromRunnable(new Runnable() {

							@Override
							public void run() {
								try {
									HttpServer2.this.connections += 1;
									HttpServer2.this.initalHandle(s);
									HttpServer2.this.connections -= 1;
								} catch (Exception e) {
									HttpServer2.this.connections -= 1;
									if (e instanceof SocketException && e.getMessage().equals("Broken pipe (Write failed)")) {
									}else {
										e.printStackTrace();
									}
								}
							}
							
						}, "$HTTPServer2.process"));
					}catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
		});
		if (this.httpsSocket != null) {
			ThreadExecutor.execute(new Runnable() {
				
				@Override
				public void run() {
					while (true) {
						try {
							final Socket s = HttpServer2.this.httpsSocket.accept();
							HttpServer2.this.td.addTask(RunnableWorker.fromRunnable(new Runnable() {

								@Override
								public void run() {
									try {
										HttpServer2.this.connections += 1;
										HttpServer2.this.initalHandle(s);
										HttpServer2.this.connections -= 1;
									} catch (Exception e) {
										HttpServer2.this.connections -= 1;
										if (e instanceof SocketException && e.getMessage().equals("Broken pipe (Write failed)")) {
										}else if (e instanceof SSLHandshakeException && 
												(e.getMessage().equals("Received fatal alert: unknown_ca")
														|| e.getMessage().equals("Received fatal alert: handshake_failure"))) {
										}else {
											e.printStackTrace();
										}
									}
								}
								
							}, "$HTTPServer2.processSSL"));
						}catch (Exception e) {
							e.printStackTrace();
						}
					}
				}
			});
		}
	}

	private final InetAddress addr;
	private final int port;
	
	public String getIP() {
		return addr.getHostAddress();
	}
	
	public int getPort() {
		return this.port;
	}
	
	public String putSetting(final String key, final String value) {
		return this.settings.put(key, value);
	}
	
	public void setServerName(final String value) {
		this.putSetting(KEY_WEB_SERVER_NAME, value);
	}
	
	public String getSetting(final String key) {
		return this.settings.get(key);
	}
	
	private String PAGE_NOT_FOUND = ErrorStates.PAGE_NOT_FOUND;
	
	public String getPageNotFound() {
		return this.PAGE_NOT_FOUND;
	}
	
	public void setPageNotFound(final String pnf) {
		if (pnf == null || pnf.length() == 0) {
			this.PAGE_NOT_FOUND = ErrorStates.PAGE_NOT_FOUND;
		}else {
			this.PAGE_NOT_FOUND = pnf;
		}
	}

	public static class ErrorStates {
		public static final String MAINTENANCE = FileUtil.getResourceAsString(ErrorStates.class, "_maintainence_mode.html");
		public static final String PAGE_NOT_FOUND = 
				FileUtil.getResourceAsString(ErrorStates.class, "_404_not_found.html");
		public static final String INTERNAL_ERROR = 
				FileUtil.getResourceAsString(ErrorStates.class, "internal_error.html");
		public static final String ACCESS_DENIED = 
				FileUtil.getResourceAsString(ErrorStates.class, "access_denied.html");
	}
	
	public boolean setMaintenance(final boolean set) {
		if (set) {
			this.override = ErrorStates.MAINTENANCE;
		}else {
			this.override = null;
		}
		return set;
	}
	
	private void initalHandle(final Socket socket) throws Exception {
		socket.setTcpNoDelay(true);
		if (this.connectionsLimit > 0 && this.connections >= this.connectionsLimit) {
			if (this.connections + 4 >= this.connectionsLimit) {
				// Too many connections, just refuse the user.
				socket.close();
				return;
			}else {
				// Inform the user about the server being too busy.
			}
		}
		final PrintWriter dos = new PrintWriter(socket.getOutputStream());
		final InputStreamReader isr = new InputStreamReader(socket.getInputStream());
		final BufferedReader reader = new BufferedReader(isr);
		final String dk = StringsUtil.randomString(32);
		if (this.override != null) {
			final String ovr = this.override
					.replaceAll(Pattern.quote("$HTTPServer2.WebServerName"), this.getSetting(KEY_WEB_SERVER_NAME))
					.replaceAll(Pattern.quote("<!--$HTTPServer2.replace.Updates-->"), "<p style=\"color: gray;\">No updates avaliable.</p>");
			dos.println("HTTP/1.1 200 OK");
			dos.println("Content-type: text/html");
			dos.println("Server-name: " + this.getSetting(KEY_WEB_SERVER_NAME));
			dos.println("Server: HttpServer2 from nullUtil 2.6");
			dos.println("iWebKit.dstream: false::forced");
			dos.println("iWebKit.decryptKey: " + dk);
			dos.println("iWebKit.contentCode: 0x2C"); // 0x2C/42 --> Overriden content.
			dos.println("Content-length: " + ovr.length());
			dos.println("");
			dos.println(ovr);
			dos.flush();
			dos.close();
			reader.close();
			return;
		}
		this.handle(socket, dos, reader, dk);
	}
	
	private boolean checkLine(final String line) {
		return line != null && !line.equals("\n") && !line.equals("\r\n");
	}
	
	private HashMap<String, String> readRequestHeaders(final BufferedReader in) throws Exception {
		final HashMap<String, String> rq = new HashMap<String, String>();
		String inp;
		while (checkLine((inp = in.readLine()))) {
			if (inp.length() == 0 || inp.equals("")) {
				break;
			}
			final String[] sp = inp.split(": ");
			rq.put(sp[0], ArrayUtils.StringArrayUtils.addFrom(sp, 1));
		}
		return rq;
	}
	
	private byte[] readExtraData(final InputStream in, final String contentLengthStr) throws Exception {
		//FIXME: 
		final int contentLength = MathUtil.safeParseI(contentLengthStr, -1);
		if (in.available() < 0 || contentLength <= 0) {
			return new byte[0];
		}
		byte[] data = new byte[contentLength];
		IOUtils.readFully(in, data, 0, contentLength);
		return data;
	}
	
	private final HashMap<String, HttpPage2> PAGES = new HashMap<String, HttpPage2>();
	
	public boolean addPage(final HttpPage2 page) {
		if (page == null) {
			return false;
		}
		this.PAGES.put(page.getID(), page);
		return true;
	}
	
	public boolean addPage(final HttpPage page) {
		if (page == null) {
			return false;
		}
		this.PAGES.put(page.getID(), HttpPage2.convert(page));
		return true;
	}
	
	public void addFileString(final String name, final String file) throws Exception {
		final HttpPage s = new HttpPage("/" + name) {
			
			@Override
			public String handleRequest(String args, String ip, RequestType type) {
				return file;
			}

			@Override
			public byte[] handleRequest_Alt(String args, String ip, RequestType type) {
				return null;
			}
		};
		this.addPage(s);
	}
	
	public void addFileBytes(final String name, final byte[] data) throws Exception {
		final HttpPage s = new HttpPage("/" + name) {
			
			@Override
			public String handleRequest(String args, String ip, RequestType type) {
				return null;
			}

			@Override
			public byte[] handleRequest_Alt(String args, String ip, RequestType type) {
				return data;
			}
		};
		s.replyOverBytes(true);
		this.addPage(s);
	}
	
	public HttpPage2 getPage(final String id) {
		final HttpPage2 inital = this.PAGES.get(id);
		return (inital != null ? inital : unknown(id));
	}
	
	public HttpPage2 unknown(final String id) {
		return null;
	}
	
	private void _invoke_404(final PrintWriter out, final String dk, final BufferedReader in) throws Exception {
		final String _404 = this.PAGE_NOT_FOUND.replaceAll(Pattern.quote("$HTTPServer2.WebServerName"), 
				this.getSetting(KEY_WEB_SERVER_NAME));
		out.println("HTTP/1.1 404 Page Not Found");
		out.println("Content-type: text/html");
		out.println("Server-name: " + this.getSetting(KEY_WEB_SERVER_NAME));
		out.println("Server: HttpServer2 from nullUtil 2.6");
		out.println("iWebKit.dstream: false::forced");
		out.println("iWebKit.decryptKey: " + dk);
		out.println("iWebKit.contentCode: 0x2D"); // 0x2D/45 --> Content Not Found
		out.println("Content-length: " + _404.length());
		out.println("");
		out.println(_404);
		out.flush();
		in.close();
		out.close();
	}
	
	private void _invoke_503(final PrintWriter out, final String dk, final BufferedReader in) throws Exception {
		final String _503 = ErrorStates.INTERNAL_ERROR.replaceAll(Pattern.quote("$HTTPServer2.WebServerName"), 
				this.getSetting(KEY_WEB_SERVER_NAME));
		out.println("HTTP/1.1 503 Internal Error Occured");
		out.println("Content-type: text/html");
		out.println("Server-name: " + this.getSetting(KEY_WEB_SERVER_NAME));
		out.println("Server: HttpServer2 from nullUtil 2.6");
		out.println("iWebKit.dstream: false::forced");
		out.println("iWebKit.decryptKey: " + dk);
		out.println("iWebKit.contentCode: 0x2D"); // 0x2D/45 --> Content Not Found
		out.println("Content-length: " + _503.length());
		out.println("");
		out.println(_503);
		out.flush();
		in.close();
		out.close();
	}
	
	private void handle(final Socket socket, final PrintWriter out, final BufferedReader in,
			final String dk) throws Exception {
		if (socket.isClosed()) {
			log.error("handle(): Uhh, the socket is closed, bailing out of this request...");
			return;
		}
		final String request0 = in.readLine();
		if (request0 == null || request0.length() == 0) {
			out.close();
			in.close();
			socket.close();
			return;
		}
		//System.out.println(request0);
		final String[] request = request0.split(" ");
		final String REQUEST_TYPE = request[0];
		String REQUEST_URI = "/";
		try {
			REQUEST_URI = request[1].split("\\?|&")[0];
		}catch (Exception e) {
			e.printStackTrace();
		}
		final HashMap<String, String> headers = readRequestHeaders(in);
		//System.out.println(headers);
		final byte[] extraData = readExtraData(socket.getInputStream(), headers.get("Content-Length"));
		//System.out.println(extraData);
		
		//ArrayUtils.printLln(headers);
		final HttpPage2 page = getPage(REQUEST_URI);
		//System.out.println(page);
		if (page == null) {
			this._invoke_404(out, dk, in);
			return;
		}
		final RequestType type = RequestType.fetch(REQUEST_TYPE);
		log.debug("Requesting page: " + page.getID());
		final DataSet<Integer, InputStream> resp = page.request(type, request[1], headers, extraData, 
				socket.getInetAddress().getHostAddress());
		if (!isValid(resp)) {
			this._invoke_503(out, dk, in);
			return;
		}
		final HttpCompressionMethod comp = HttpCompressionMethod.parse(headers.get("Accept-Encoding"));
		log.debug("Accept-Encoding: " + comp);
		out.println("HTTP/1.1 " + resp.getA());
		out.println("Server-name: " + this.getSetting(KEY_WEB_SERVER_NAME));
		out.println("Server: HttpServer2 from nullUtil 2.6");
		out.println("iWebKit.dstream: false::forced");
		out.println("iWebKit.decryptKey: " + dk);
		out.println("iWebKit.contentCode: 0x2B"); // 0x2B/43 --> Content Found
		this.customHeaders(out);
		String mime = page.getMimeType();
		if (mime == null) {
			mime = URLConnection.guessContentTypeFromStream(resp.getB());
		}
		if (mime != null) {
			out.println("Content-type: " + mime);
		}
		out.println("Access-Control-Allow-Origin: *");
		out.println("Access-Control-Allow-Credentials: true");
		out.println("Content-length: " + resp.getB().available());
		if (comp == HttpCompressionMethod.NONE) {
			out.println();
			out.flush();
			try {
				IOUtils.copy(resp.getB(), socket.getOutputStream());
			}catch (Exception e) {
				if (!e.getMessage().contains("Connection reset") && !e.getMessage().contains("Broken pipe (Write failed)")) {
					e.printStackTrace();
				}
			}
			//IOUtils.copy(resp.getB(), socket.getOutputStream());
			socket.getOutputStream().flush();
		}else if (comp == HttpCompressionMethod.DEFLATE) {
			out.println("Content-Encoding: deflate");
			out.println();
			out.flush();
			final ZipOutputStream zos = new ZipOutputStream(socket.getOutputStream());
			IOUtils.copy(resp.getB(), zos);
			zos.flush();
			resp.getB().close();
			socket.getOutputStream().flush();
		}/*else if (comp == HttpCompressionMethod.GZIP) {
			out.println("Content-Encoding: gzip");
			out.println();
			out.flush();
			final GZIPOutputStream gzos = new GZIPOutputStream(socket.getOutputStream());
			IOUtils.copy(resp.getB(), gzos);
			gzos.flush();
			resp.getB().close();
			socket.getOutputStream().flush();
		}*/else if (comp == HttpCompressionMethod.IWEB_ENC) {
			out.println("Content-Encoding: iweb_enc");
			out.println("Content-Encoding-Version: 1.1");
			out.println();
			out.flush();
			//socket.getOutputStream().write(
			//		EncryptAPI.getEncryptorForEncryption("XXTEA").encryptBytes(resp.getB(), dk));
			byte[] buffer = new byte[4096];
			while (resp.getB().read(buffer) != -1) {
				socket.getOutputStream().write(EncryptAPI.getEncryptorForEncryption("XXTEA")
						.encryptBytes(buffer, dk));
				socket.getOutputStream().flush();
			}
			resp.getB().close();
			socket.getOutputStream().flush();
		}else if (comp == HttpCompressionMethod.iWebKitSecure && headers.containsKey("iWebKit.Secure.Method")
				&& headers.containsKey("iWebKit.publicKey")) {
			out.println("Content-Encoding: iWebKitSecure");
			final String publicKey = headers.get("iWebKit.publicKey");
			log.debug("Public Key: " + publicKey);
			final String method = headers.get("iWebKit.Secure.Method");
			log.debug("Method: " + method);
			if (method.equalsIgnoreCase("RSA_2048") || method.equalsIgnoreCase("RSA_4096")) {
				out.println("Content-Encoding-Type: " + (method.equalsIgnoreCase("RSA_4096") ? 
						"iWebKitSecure.RSA_4096" : "iWebKitSecure.RSA_2048"));
				out.println();
				//FIXME: Implement me correctly
				final PublicKey pk = RSA.getPublicKey(ByteUtils.fromBase64(publicKey));
				byte[] buffer = new byte[4096];
				while (resp.getB().read(buffer) != -1) {
					final byte[] enc = RSA.encrypt(pk, buffer);
					socket.getOutputStream().write(enc);
					socket.getOutputStream().flush();
				}
				resp.getB().close();
			}else if (method.equalsIgnoreCase("AES_256")) {
				final AESCipher aes = new AESCipher(publicKey.getBytes());
				out.println("Content-Encoding-Type: iWebKitSecure.AES_256");
				out.println();
				byte[] buffer = new byte[4096];
				while (resp.getB().read(buffer) != -1) {
					final byte[] enc = aes.getEncryptedMessage(buffer);
					socket.getOutputStream().write(enc);
					socket.getOutputStream().flush();
				}
				resp.getB().close();
			}else {
				out.println();
				log.error("Cannot provide iWebKit Secure to a user, they didn't provide a supported method!");
			}
			//TODO: Inform about RSA type
			//TODO: Add encryption by 8KB buffers
			out.flush();
		}else {
			out.println();
			out.flush();
			try {
				IOUtils.copy(resp.getB(), socket.getOutputStream());
			}catch (Exception e) {
				if (!e.getMessage().contains("Connection reset") && !e.getMessage().contains("Broken pipe (Write failed)")) {
					e.printStackTrace();
				}
			}
//			IOUtils.copy(resp.getB(), socket.getOutputStream());
			socket.getOutputStream().flush();
		}
		socket.getOutputStream().flush();
		out.close();
		in.close();
	}
	
	protected void customHeaders(final PrintWriter out) {
	}

	private boolean isValid(final DataSet<Integer, InputStream> resp) {
		return resp != null && resp.getA() > 100 && resp.getB() != null;
	}

	public void addFileRAW(final String name, final File file) {
		final HttpPage s = new HttpPage("/" + name, file) {
			
			@Override
			public String handleRequest(String args, String ip, RequestType type) {
				return this.getData();
			}

			@Override
			public byte[] handleRequest_Alt(String args, String ip, RequestType type) {
				return null;
			}
		};
		//s.loadFromFile(file);
		s.setSendRaw(true);
		s.setMime(MimeType.fromFile(file).getType());
		this.addPage(s);
	}

	public Logger getLogger() {
		return this.log;
	}
}
